/**
 * Copyright (c) 2019-2030 panguBpm All rights reserved.
 * <p>
 * http://www.pangubpm.com/
 * <p>
 * （盘古BPM工作流平台）
 */
package com.pangubpm.bpm.model.xml.impl.util;

import com.pangubpm.bpm.model.xml.ModelParseException;
import com.pangubpm.bpm.model.xml.impl.ModelInstanceImpl;
import com.pangubpm.bpm.model.xml.impl.instance.DomDocumentImpl;
import com.pangubpm.bpm.model.xml.impl.instance.DomElementImpl;
import com.pangubpm.bpm.model.xml.instance.DomDocument;
import com.pangubpm.bpm.model.xml.instance.DomElement;
import com.pangubpm.bpm.model.xml.instance.ModelElementInstance;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Helper methods which abstract some gruesome DOM specifics.
 * It does not provide synchronization when invoked in parallel with
 * the same objects.
 *
 * @author pangubpm(pangubpm@139.com)
 * @author pangubpm( pangubpm@139.com )
 *
 */
public final class DomUtil {

  /**
   * A {@link NodeListFilter} allows to filter a {@link NodeList},
   * retaining only elements in the list which match the filter.
   *
   * @see DomUtil#filterNodeList(NodeList, NodeListFilter)
   */
  public interface NodeListFilter {

    /**
     * Test if node matches the filter
     *
     * @param node the node to match
     * @return true if the filter does match the node, false otherwise
     */
    boolean matches(Node node);

  }

  /**
   * Filter retaining only Nodes of type {@link Node#ELEMENT_NODE}
   *
   */
  public static class ElementNodeListFilter implements NodeListFilter {

    public boolean matches(Node node) {
      return node.getNodeType() == Node.ELEMENT_NODE;
    }

  }

  /**
   * Filters {@link Element Elements} by their nodeName + namespaceUri
   *
   */
  public static class ElementByNameListFilter extends ElementNodeListFilter {

    private final String localName;
    private final String namespaceUri;

    /**
     * @param localName the local name to filter for
     * @param namespaceUri the namespaceUri to filter for
     */
    public ElementByNameListFilter(String localName, String namespaceUri) {
      this.localName = localName;
      this.namespaceUri = namespaceUri;
    }

    @Override
    public boolean matches(Node node) {
     return super.matches(node)
        && localName.equals(node.getLocalName())
        && namespaceUri.equals(node.getNamespaceURI());
    }

  }

  public static class ElementByTypeListFilter extends ElementNodeListFilter {

    private final Class<?> type;
    private final ModelInstanceImpl model;

    public ElementByTypeListFilter(Class<?> type, ModelInstanceImpl modelInstance) {
      this.type =  type;
      this.model = modelInstance;
    }

    @Override
    public boolean matches(Node node) {
      if (! super.matches(node)) {
        return false;
      }
      ModelElementInstance modelElement = ModelUtil.getModelElement(new DomElementImpl((Element) node), model);
      return type.isAssignableFrom(modelElement.getClass());
    }
  }

  /**
   * Allows to apply a {@link NodeListFilter} to a {@link NodeList}. This allows to remove all elements from a node list which do not match the Filter.
   *
   * @param nodeList the {@link NodeList} to filter
   * @param filter the {@link NodeListFilter} to apply to the {@link NodeList}
   * @return the List of all Nodes which match the filter
   */
  public static List<DomElement> filterNodeList(NodeList nodeList, NodeListFilter filter) {

    List<DomElement> filteredList = new ArrayList<DomElement>();
    for(int i = 0; i< nodeList.getLength(); i++) {
      Node node = nodeList.item(i);
      if(filter.matches(node)) {
        filteredList.add(new DomElementImpl((Element) node));
      }
    }

    return filteredList;

  }

  /**
   * Filters a {@link NodeList} retaining all elements
   *
   * @param nodeList  the the {@link NodeList} to filter
   * @return the list of all elements
   */
  public static List<DomElement> filterNodeListForElements(NodeList nodeList) {
    return filterNodeList(nodeList, new ElementNodeListFilter());
  }

  /**
   * Filter a {@link NodeList} retaining all elements with a specific name
   *
   *
   * @param nodeList the {@link NodeList} to filter
   * @param namespaceUri the namespace for the elements
   * @param localName the local element name to filter for
   * @return the List of all Elements which match the filter
   */
  public static List<DomElement> filterNodeListByName(NodeList nodeList, String namespaceUri, String localName) {
    return filterNodeList(nodeList, new ElementByNameListFilter(localName, namespaceUri));
  }

  /**
   * Filter a {@link NodeList} retaining all elements with a specific type
   *
   *
   * @param nodeList  the {@link NodeList} to filter
   * @param modelInstance  the model instance
   * @param type  the type class to filter for
   * @return the list of all Elements which match the filter
   */
  public static List<DomElement> filterNodeListByType(NodeList nodeList, ModelInstanceImpl modelInstance, Class<?> type) {
    return filterNodeList(nodeList, new ElementByTypeListFilter(type, modelInstance));
  }

  public static class DomErrorHandler implements ErrorHandler {

    private static final Logger LOGGER = Logger.getLogger(DomErrorHandler.class.getName());

    private String getParseExceptionInfo(SAXParseException spe) {
      return "URI=" + spe.getSystemId() + " Line="
        + spe.getLineNumber() + ": " + spe.getMessage();
    }

    public void warning(SAXParseException spe) {
      LOGGER.warning(getParseExceptionInfo(spe));
    }

    public void error(SAXParseException spe) throws SAXException {
      String message = "Error: " + getParseExceptionInfo(spe);
      throw new SAXException(message);
    }

    public void fatalError(SAXParseException spe) throws SAXException {
      String message = "Fatal Error: " + getParseExceptionInfo(spe);
      throw new SAXException(message);
    }
  }

  /**
   * Get an empty DOM document
   *
   * @param documentBuilderFactory the factory to build to DOM document
   * @return the new empty document
   * @throws ModelParseException if unable to create a new document
   */
  public static DomDocument getEmptyDocument(DocumentBuilderFactory documentBuilderFactory) {
    try {
      DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
      return new DomDocumentImpl(documentBuilder.newDocument());
    } catch (ParserConfigurationException e) {
      throw new ModelParseException("Unable to create a new document", e);
    }
  }

  /**
   * Create a new DOM document from the input stream
   *
   * @param documentBuilderFactory the factory to build to DOM document
   * @param inputStream the input stream to parse
   * @return the new DOM document
   * @throws ModelParseException if a parsing or IO error is triggered
   */
  public static DomDocument parseInputStream(DocumentBuilderFactory documentBuilderFactory, InputStream inputStream) {

    try {
      DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
      documentBuilder.setErrorHandler(new DomErrorHandler());
      return new DomDocumentImpl(documentBuilder.parse(inputStream));
    } catch (ParserConfigurationException e) {
      throw new ModelParseException("ParserConfigurationException while parsing input stream", e);

    } catch (SAXException e) {
      throw new ModelParseException("SAXException while parsing input stream", e);

    } catch (IOException e) {
      throw new ModelParseException("IOException while parsing input stream", e);

    }
  }

}
